iT邦幫忙

2024 iThome 鐵人賽

DAY 28
0
自我挑戰組

NLP 新手的 30 天入門養成計畫系列 第 28

[Day 28] - 把 IR 也加進來!檢索增強生成 (2)

  • 分享至 

  • xImage
  •  

今天要繼續把 RAG 實作的部分完成。

我們之前提到,RAG 包含了 Retriever 和 Generator 兩個部分,也就是將檢索到的相關文檔傳給模型生成回應,而我們第一件要做的事情就是蒐集文檔。

Langchain 提供很多種傳入資料的方式,包含 json、txt、xml、html 等等,而在這次的實作中,我想要以網頁爬蟲的做法來建立資料集。

雖然 Langchain 也有自己的 web loader 方法,不過我想了想還是沿用 Day 2 的爬蟲寫法比較習慣,而資料來源同樣也是從台灣英文新聞網獲取,因為我發現它的格式蠻統一的,每一篇新聞都有對應的 ID,所以我選了五篇新聞當作資料集:

import requests
from bs4 import BeautifulSoup

def get_documents(id):
  url = 'https://www.taiwannews.com.tw/news/' + id
  headers = {'User-Agent': 'Mozilla/5.0'}
  response = requests.get(url, headers = headers)

  if response.status_code == 200:
    soup = BeautifulSoup(response.text, 'html.parser')
    article = soup.find('div', class_ = 'editor __className_11742b')
    paragraphs = article.find_all('p')
    content = ' '.join([p.get_text() for p in paragraphs])
    return content

news = ['5914017', '5924769', '5924648', '5924515', '5923187']
documents = []

for id in news:
  content = get_documents(id)
  documents.append(content)

在 documents 中,我們把這五篇新聞收錄了下來,在接下來的檢索流程中,我分成了兩種方式:

  1. Sparse Retrieval

    如果有看 Day 13 文章的話會知道,我介紹了 BM25 的基本概念,以及在 Day 14 的文章中用 Pyserini 建立反向索引來實作 BM25。

    而這次不用這麼麻煩了,因為 Langchain 也有 BM25 的工具可以使用,我查了一下它對於 BM25Retriever 的 source code,裡面確實連 preprocess_func 都有先預設好,一般情況下直接調用就可以了。

    from langchain_community.retrievers import BM25Retriever
    
    retriever = BM25Retriever.from_texts(documents, k = 1)
    query = "What was the main focus of OpenAI's visit to National Chengchi University?"
    retriever.invoke(query)
    
    [Document(page_content='TAIPEI (Taiwan News) — National Chengchi University (NCCU) received a visit from OpenAI on Friday (Aug. 23) and scholars were shown a demonstration of the company’s latest ChatGPT product speaking in Mandarin Chinese with a Taiwanese accent. OpenAI’s Asia Pacific public policy Director Sandy Kunvatangarn visited NCCU for the demonstration and a discussion about AI, per CNA...')]
    

    因為我事先看過這些文章,裡面有一篇是關於好幾天前 OpenAI 人員拜訪政大的新聞,所以就拿它來設計問題了。此外,由於正確的文章只有一篇,所以我把 top-k 設為 1,避免在後面流程中,LLM 接收到其它不重要的訊息。

    而 BM25 也的確找出了正確的文章,因此我們又可以朝著下一步邁進,不過因為 generating 的部分和 Dense Retrieval 重複到了,所以我只做檢索的部分,大家可以自己用 retriever 把 LLM 接上去。

    接著來看看 Dense Retrieval 是怎麼做的吧。

  2. Dense Retrieval

    對於密集檢索,我們就需要先將這些文章 embedding 成向量,然後儲存起來,需要的時候就可以拿來檢索。我看 Langchain 在官方文檔中使用的是 Chroma,而我使用的是 FAISS,兩者都有向量資料庫 ( vector db ) 和檢索的功能,而且用法一模一樣:

    from langchain_community.vectorstores import FAISS # 也可以換成 Chroma
    from langchain_openai import OpenAIEmbeddings
    from langchain_text_splitters import CharacterTextSplitter
    
    text_splitter = CharacterTextSplitter(chunk_size = 1000, chunk_overlap = 0)
    texts = text_splitter.create_documents(documents)
    embeddings = OpenAIEmbeddings(model = "text-embedding-3-small")
    
    db = FAISS.from_documents(texts, embeddings) # 也可以換成 Chroma
    retriever = db.as_retriever(search_kwargs = {"k": 1}) # Top-k
    

    這裡要特別介紹的是 CharacterTextSplitter 的功能。事實上,在 LLM 剛推出的時候,由於它一次可以接受的訊息量沒有到很多,可能 4096 個 tokens 之類的,因此如果要給它一整部小說,然後詢問它其中一小部分內容的話,需要先將整部小說切割成好幾個 chunks,然後再用檢索的方式找到那一段資料來回答,所以才需要 CharacterTextSplitter。

    不過現在模型越做越大,能夠處理的訊息量,也就是上下文視窗 ( context window ) 也越來越大,我們可以在 OpenAI 的官網找到模型相關的說明:

    https://ithelp.ithome.com.tw/upload/images/20240902/20159088UXS5pa2ZJz.png

    像 GPT-4o 就達到了 128000 個 tokens 這麼多,訓練的資料也更新到了 2023 年。

    回到正題,在 CharacterTextSplitter 的參數中,chunk_size 是用來設定你想要切割的每一個 chunk 的大小,chunk_overlap 代表你想要 chunks 之間要有多少重疊的內容,此外,你還可以設定要以哪些字元作為切割標準,像是換行或句號等等。

    在 embedding 的部分,OpenAI 提供了很多模型,我選擇的是 text-embedding-3-small。此外,和 Sparse Retrieval 一樣,如果只是想要找到檢索文檔的話執行 retriever.invoke(query) 就好。

    最後再把檢索到的文章接上 LLM 生成回應,下面這段程式碼出自於官方提供的範例,不過我在 prompt 和 llm 設定的地方換成自己的:

    from langchain_core.output_parsers import StrOutputParser
    from langchain_core.runnables import RunnablePassthrough
    from langchain.chat_models import ChatOpenAI
    from langchain.chains import LLMChain
    from langchain.prompts import PromptTemplate
    
    def format_docs(docs):
        return "\n\n".join(doc.page_content for doc in docs)
    
    llm = ChatOpenAI(model_name = "gpt-3.5-turbo", temperature = 0.9)
    template = 
    """Use the following content to answer the question. If the answer is not found, just say that you don't know. Use only one sentences and keep the answer concise.
    Question: {question}
    Context: {context}
    Answer:
    """
    prompt = PromptTemplate(
      input_variables = ["context", "question"],
      template = template
    )
    
    rag_chain = (
        {"context": retriever 
        | format_docs, "question": RunnablePassthrough()} 
        | prompt 
        | llm 
        | StrOutputParser()
    )
    
    query = "What was the main focus of OpenAI's visit to National Chengchi University?"
    rag_chain.invoke(query)
    
    """The main focus of OpenAI's visit to National Chengchi University was to demonstrate the company's latest ChatGPT product speaking in Mandarin Chinese with a Taiwanese accent and to discuss AI with scholars."""
    

    最終,模型找到了正確的新聞,生成出來的答案也非常正確,而且用詞和原文很相近,符合我們一開始想要使用 RAG 的目標。

    我在跑這段程式碼的時候也嘗試在不提供這篇新聞的情況下,把一模一樣的問題丟給 ChatGPT,而他不出所料的要嘛說不知道,要嘛生成一些很模糊沒有依據的回應。

以上就是關於 RAG 的簡單實作,事實上 Langchain 還提供了很多不同的檢索方式以及其他可以提升 LLM 回答表現的工具,大家有興趣也可以試著玩玩看哦。

資料來源
Taiwan News

推薦文章

PS : 明天就要進入最後一個主題啦!


上一篇
[Day 27] - 把 IR 也加進來!檢索增強生成 (1)
下一篇
[Day 29] - 如何判斷 LLM 有沒有亂回答
系列文
NLP 新手的 30 天入門養成計畫30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言